Skip to content

#105 Allow Procs for dynamic value usage #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Oct 5, 2022

Conversation

codegeek319
Copy link

This PR allows Procs for the value determinations at runtime. Also the Test and the Documentation is updated according to the latest code.
closes #105

@@ -31,7 +35,7 @@ def types
end
end

def types_to_human_format
def types_to_human_format(types)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codegeek319 probably naming should be changed, or code refactored

because we have method "types", and we have parameter types and local variables.

this is confusing. Do we need to pass types everywhere?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@igorkasyanchuk sorry for the late answer and tackling of this issue!
For the dynamic proc evaluation, we need to check the options for procs on every validation, call them, then continue the validations with the new options. As the options are used in the types method to determine the authorized types, we now have to pass the new options to it and the resulting authorized types can be different on each validation - that's why we then pass the types on to the methods which need them.

I renamed the types method to authorized_types to avoid confusion with variables and parameters. Looks clearer now, I agree. :)

@gr8bit
Copy link
Collaborator

gr8bit commented Feb 4, 2022

@igorkasyanchuk would you mind to take another look? Thanks! :)

@igorkasyanchuk
Copy link
Owner

Hello @gr8bit ,

Sorry for taking so long time, I think I've some important comments now.
I see in the code that often you use something like range = range.call(record) if range.is_a?(Proc) and you checking in many places objects if it's proc or now. Because of it you changed lot's of code, and added some complexity to existing method, for example by adding new parameters.

I've the following idea, it probably can take not long time to implement it, at least I want you to think if it could work better.
So the idea is the following - validation class receives some "options", which are consist of hash and potentially hashes inside.

We can just convert all hashes to values before. So no need to call proc somewhere inside validator. Everything will keep working as before.

I did some quick prototype, you can use it as a reference and improve. Ready to discuss it with you, If needed in skуpe "igorkasyanchuk".

image

@igorkasyanchuk
Copy link
Owner

@codegeek319 sorry mentioning you too. Just in case you need:

    def recur_call_hash(record, h)
      hash = h.dup
      return hash unless hash.is_a?(Hash)

      puts hash
      hash.each do |(k,v)|
        if v.is_a?(Proc)
          hash[k] = v.call(record)
        else
          hash[k] = v
        end
        if hash[k].is_a?(Hash)
          hash[k] = recur_call_hash(record, hash[k])
        end
      end
      hash
    end

@igorkasyanchuk
Copy link
Owner

and last suggestion, to make in Readme example more close to real life if possible. For example

validates :proc_files, limit: { max: -> (record) { record.admin? ? 100 : 10 } }

@gr8bit
Copy link
Collaborator

gr8bit commented Feb 6, 2022

Thank you for the great feedback!! @codegeek319 has been implementing this and we'll discuss it on Tuesday, we work together irl. :)

@igorkasyanchuk
Copy link
Owner

@gr8bit @codegeek319 do you have plans to fix this PR?

@gr8bit
Copy link
Collaborator

gr8bit commented Apr 17, 2022

Sure thing! Sorry, we have been quite busy ☺️

…ons into allow_procs

* 'master' of github.com:bichinger/active_storage_validations: (32 commits)
  Create zh-CN.yml
  Update README.md
  Update README.md
  preparation for a release
  Add test for Dimension validation errors
  Display All Validation Errors
  Update README.md
  rm .ruby-version
  Fixes
  Fix tests
  Update metadata.rb
  Update metadata.rb
  Dynamic exception_class
  Update README.md
  Fix a file extension issue in Metadata#read_image
  prepeation for a release
  prepeation for a release
  preparing for a release
  Test against Rails 7.0 and Ruby 3.1
  Remove references to image/jpg content type
  ...

# Conflicts:
#	lib/active_storage_validations/aspect_ratio_validator.rb
#	lib/active_storage_validations/dimension_validator.rb
#	test/dummy/app/models/only_image.rb
@gr8bit
Copy link
Collaborator

gr8bit commented Sep 10, 2022

@igorkasyanchuk finally I found the time for the refactoring you suggested. Diff shrunk significantly. Would you mind taking another look? :)

@igorkasyanchuk
Copy link
Owner

thank you for your PR, from my side I think it looks good

But I want to check with other developers who did the most contributions to the gem

@StefSchenkelaars , @phlegx , @tleneveu , @reckerswartz what do you think about this PR?

Copy link
Contributor

@StefSchenkelaars StefSchenkelaars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea! Makes it more similar to Active Model so that's good.
Code wise I think it should also be a bit more like Active Model. Especially the local version of the options variable seems dangerous to me since it gives quite some ambiguity.
So try to ensure the instance methods are actually instance methods and are not used as some helper method which still accesses instance variables.
If you have any questions about my comments or need a bit of help implementing, let me know!

@@ -24,11 +26,12 @@ def validate_each(record, attribute, _value)
changes = record.attachment_changes[attribute.to_s]
return true if changes.blank?

options = unfold_procs(record, self.options, AVAILABLE_CHECKS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like changing the original options variable. The options variable is made available in the ActiveModel::Validator and I would really keep it that way.

Copy link
Collaborator

@gr8bit gr8bit Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope I understand you correctly but the original options variable will not be changed, it will be masked by a local variable called options. To avoid this confusion, I'll rename the local variable to flat_options.

@@ -4,6 +4,8 @@

module ActiveStorageValidations
class AspectRatioValidator < ActiveModel::EachValidator # :nodoc
include OptionProcUnfolding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reuse the code and logic from active model? Since we depend on it anyway, why not use that a bit more. For example the https://github.com/rails/rails/blob/d695972025146713fae9a089dcaf2239ede354d4/activemodel/lib/active_model/validations/resolve_value.rb#L6 method might be useful.

Copy link
Collaborator

@gr8bit gr8bit Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That method doesn't descend into Hashes and Arrays and tries to call Symbols as methods. That's not what we need for our "options-processor". (Also it isn't yet part of any rails stable version.)
If it wasn't for Rails 5.2 compatibility I would have used deep_transform_values (https://apidock.com/rails/v6.0.0/Hash/deep_transform_values), but that was added in Rails 6. :(

files = Array.wrap(changes.is_a?(ActiveStorage::Attached::Changes::CreateMany) ? changes.attachables : changes.attachable)

files.each do |file|
metadata = Metadata.new(file).metadata
next if is_valid?(record, attribute, metadata)
next if is_valid?(record, attribute, metadata, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This options is an instance variable (or at least it should be). So passing this should be unnecessary. If the unfolding of the options has to be done, do that in the location where you need it (in the private method).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it is not the instance variable, it's the local variable option which was masked above. Which I will rename to flat_options to make it clearer.

Comment on lines 30 to 31
def authorized_types(options)
(Array.wrap(options[:with]) + Array.wrap(options[:in])).compact.map do |type|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an instance method but is actually used as a helper. I would really try to make clear that these authorized types are dependent on the record (which they are implicitly since the options depend on the record). I would therefore rewrite this method to something like this:

def authorized_types(record)
  with = resolve_value(record, options[:with])
  in = resolve_value(record, options[:in])
  
  (Array.wrap(with) + Array.wrap(in)).compact.map do |type|

This makes it clear that the authorized types depend on the record and you don't change the original options.

Copy link
Collaborator

@gr8bit gr8bit Sep 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the authorized types depend on the options (=flat_options now) which have to be passed in. We're using this same pattern in every other validator:

flat_options = unfold_procs(record, self.options, AVAILABLE_CHECKS)

The AVAILABLE_CHECKS constant specifies which keys from the options should be resolved (which, in this case, are %i[with in]). Please check it now after the rename, I think it's much more understandable now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree, the authorized types in the end depends on the record (and the options instance variable but that doesn't have to be passed).

You can generate these flat options if you have the options and the record.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I just moved the proc unfolding call into the authorized_types and pass the record inside that instead of the unfolded options.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a lot clearer. Hopefully, you don't mind me pushing on this but I find it important that the lowest level of dependencies (arguments) for each function are used. This way it is harder to screw up, for example by passing the flat_options that were generated with a different record.

@gr8bit
Copy link
Collaborator

gr8bit commented Sep 14, 2022

@StefSchenkelaars I commented on and coded towards your suggestions. :) What do you think?

@StefSchenkelaars
Copy link
Contributor

I still don't really like the passing of these options all the time but I'm ok if it works.

@gr8bit
Copy link
Collaborator

gr8bit commented Sep 24, 2022

I still don't really like the passing of these options all the time but I'm ok if it works.

I'm totally open to suggestions! Please check the validators and find a better/easier way - keep in mind though the options have to be processed on every validation.

Copy link
Contributor

@StefSchenkelaars StefSchenkelaars left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, some remarks in the same line.

@@ -54,45 +53,45 @@ def validate_each(record, attribute, _value)
private


def is_valid?(record, attribute, metadata)
def is_valid?(record, attribute, metadata, flat_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This additional depencency is not needed for this method. You can calculate the flat_options in this method since you have the record already.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I changed it.

break
end
end
end


def is_valid?(record, attribute, file_metadata)
def is_valid?(record, attribute, file_metadata, flat_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, you already have the record. Less arguments = better :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and changed :)

return if record.errors.added?(attribute, key)
record.errors.add(attribute, key, **attrs)
def add_error(record, attribute, default_message, **attrs)
message = options[:message].presence || default_message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an interesting possibility is to allow a proc for the message as well. But lets do that in a separate PR if people would like that idea.

@gr8bit
Copy link
Collaborator

gr8bit commented Oct 4, 2022

I think this PR could be merged now.

@StefSchenkelaars
Copy link
Contributor

@igorkasyanchuk Agree with @gr8bit that it's good for a merge. I think you are the only one who can do that.

@igorkasyanchuk
Copy link
Owner

Great) will do it later today
Thanks all for tremendous amount of work

@igorkasyanchuk igorkasyanchuk merged commit c439869 into igorkasyanchuk:master Oct 5, 2022
@igorkasyanchuk
Copy link
Owner

active_storage_validations-1.0.0.gem released

@igorkasyanchuk
Copy link
Owner

looks like one regression #166 if someone can take a look on this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow Procs as values for validator restrictions
4 participants